1 /**
2 Keyed item is used in combination with KeyedCollection to mimic databases
3 in your classes. This module contains:
4   $(TOC KeyedItem)
5 
6 License: $(GPL2)
7 
8 Authors: Matthew Armbruster
9 
10 $(B Source:) $(SRC $(SRCFILENAME))
11 
12 Copyright: 2016
13  */
14 module db_constraints.keyed.keyeditem;
15 
16 import std.traits : isInstanceOf;
17 
18 public import db_constraints.constraints;
19 import db_constraints.utils.meta : hasMembersWithUDA;
20 
21 /**
22 Use this in the singular class which would describe a row in your
23 database. ClusteredIndexAttribute is the unique constraint associated
24 with the clustered index.
25  */
26 mixin template KeyedItem(ClusteredIndexAttribute = PrimaryKeyColumn)
27     if (isInstanceOf!(UniqueConstraintColumn, ClusteredIndexAttribute))
28 {
29     import std.algorithm : among, canFind;
30     import std.conv : to;
31     import std.exception : collectException, enforceEx;
32     import std.functional : unaryFun;
33     import std.meta : Erase;
34     import std.signals;
35     import std.string : lastIndexOf;
36     import std.traits : isInstanceOf;
37 
38     import db_constraints.db_exceptions : CheckConstraintException;
39     import db_constraints.utils.meta;
40 
41     final private alias T = typeof(this);
42     private bool _containsChanges;
43     private ClusteredIndex _key;
44 
45     static assert(hasMembersWithUDA!(T, ClusteredIndexAttribute),
46                   "Must have columns with @UniqueConstraintColumn!\"" ~
47                   ClusteredIndexAttribute.name ~ "\" to use this mixin.");
48 
49 /**
50 The setter should be in your setter member. This checks your check
51 constraint and notifies the item if it is different and does not
52 violate the check constraint.
53 
54 $(THROWS CheckConstraintException, if your value makes checkConstraints fail.)
55  */
56     final private void setter(P)(ref P member, P value, string name_ = __FUNCTION__)
57     {
58         if (value != member)
59         {
60             P memberValue = member;
61             member = value;
62             auto ex = collectException!CheckConstraintException(checkConstraints());
63             if (ex is null)
64             {
65                 string name = name_[lastIndexOf(name_, '.') + 1 .. $];
66                 notify(name);
67             }
68             else
69             {
70                 member = memberValue;
71                 throw ex;
72             }
73         }
74     }
75 /**
76 Initializes the keyed item by running $(SRCTAG setClusteredIndex) and
77 $(SRCTAG checkConstraints).
78 This should be in your constructor.
79 
80 $(THROWS CheckConstraintException, if a member violates their constraint.)
81  */
82     final private void initializeKeyedItem()
83     {
84         setClusteredIndex();
85         checkConstraints();
86     }
87 
88 
89 /**
90 Read-only property telling if $(D this) contains changes.
91 Returns:
92     true if $(D this) contains changes.
93  */
94     final @property bool containsChanges() const nothrow pure @safe @nogc
95     {
96         return _containsChanges;
97     }
98 /**
99 Changes $(D this) to not contain changes. Should only
100 be used after a save.
101  */
102     final void markAsSaved() nothrow pure @safe @nogc
103     {
104         _containsChanges = false;
105     }
106 /**
107 The signal used to emit changes that occur in $(D this).
108  */
109     mixin Signal!(string, typeof(_key)) emitChange;
110 
111 /**
112 Notifies $(D this) which property changed. If the property is
113 part of the clustered index then the clustered index is updated.
114 This also emits a signal with the property name that changed
115 along with the clustered index.
116 Params:
117     propertyName = the property name that changed
118  */
119     final void notify(string propertyName)
120     {
121         _containsChanges = true;
122         emitChange.emit(propertyName, _key);
123         if (GetMembersWithUDA!(T, ClusteredIndexAttribute).canFind(propertyName))
124         {
125             emitChange.emit("key", _key);
126             setClusteredIndex();
127         }
128         foreach(name; Erase!(ClusteredIndexAttribute.name, GetUniqueConstraintStructNames!(T)))
129         {
130             if (GetMembersWithUDA!(T, UniqueConstraintColumn!name).canFind(propertyName))
131             {
132                 emitChange.emit(name ~ "_key", _key);
133             }
134         }
135         foreach(fk; GetForeignKeys!(T))
136         {
137             if (fk.columnNames.canFind(propertyName))
138             {
139                 emitChange.emit(fk.name ~ "_key", _key);
140             }
141         }
142     }
143 /**
144 Checks if any of the members of $(D T) have values that violate their
145 check constraint.
146 
147 $(THROWS CheckConstraintException, if the constraint is violated.)
148  */
149     final void checkConstraints()
150     {
151         foreach(member; __traits(derivedMembers, T))
152         {
153             static if (__traits(compiles, __traits(getMember, T, member)))
154             {
155                 foreach(ov; __traits(getOverloads, T, member))
156                 {
157                     foreach(attr; __traits(getAttributes, ov))
158                     {
159                         static if (isInstanceOf!(CheckConstraint, attr))
160                         {
161                             static if (attr.name.among!("NotNull", "Set", "Enum"))
162                             {
163                                 enum msg = T.stringof ~ "." ~ member ~
164                                     " " ~ attr.name ~ " violation.";
165 
166                             }
167                             else static if (attr.name == "")
168                             {
169                                 enum msg = "chk_" ~ T.stringof ~ "_" ~ member ~
170                                     " violation.";
171                             }
172                             else
173                             {
174                                 enum msg = attr.name ~ " violation.";
175                             }
176                             enforceEx!(CheckConstraintException)(
177                                     attr.check(mixin("this._" ~ member)), msg);
178                         }
179                     }
180                 }
181             }
182         }
183         foreach(attr; __traits(getAttributes, T))
184         {
185             static if (isInstanceOf!(CheckConstraint, attr))
186             {
187                 static if (attr.name.among!("NotNull", "Set", "Enum"))
188                 {
189                     enum msg = T.stringof ~ " " ~ attr.name ~ " violation.";
190 
191                 }
192                 else static if (attr.name == "")
193                 {
194                     enum msg = "chk_" ~ T.stringof ~
195                         " violation.";
196                 }
197                 else
198                 {
199                     enum msg = attr.name ~ " violation.";
200                 }
201                 enforceEx!(CheckConstraintException)(
202                         attr.check(this), msg);
203             }
204         }
205     }
206 
207 /**
208 Clustered index struct created at compile-time.
209 This is used to compare classes. The members
210 are the members of the class marked with the
211 attribute selected as the Clustered Index.
212  */
213     final struct ClusteredIndex
214     {
215         // creates the members of the clustered key with appropriate type.
216         mixin(function string()
217               {
218                   string result = "";
219                   foreach(columnName; GetMembersWithUDA!(T, ClusteredIndexAttribute))
220                   {
221                       result ~= "typeof(" ~ T.stringof ~ "._" ~ columnName ~ ") " ~ columnName ~ ";\n";
222                   }
223                   return result;
224               }());
225         // adds the generic comparison for structs
226         mixin opAAKey!(ClusteredIndex);
227     }
228 
229 
230 /**
231 The clustered index property for the class.
232 Returns:
233     The clustered index for the class.
234  */
235     final @property ClusteredIndex key() const nothrow pure @safe @nogc
236     {
237         return _key;
238     }
239 
240 /**
241 Sets the clustered index for $(D this).
242  */
243     final void setClusteredIndex() nothrow pure @safe @nogc
244     {
245         auto new_key = ClusteredIndex();
246         mixin(function string()
247               {
248                   string result = "";
249                   foreach(pkcolumn; GetMembersWithUDA!(T, ClusteredIndexAttribute))
250                   {
251                       result ~= "new_key." ~ pkcolumn ~ " = this._" ~ pkcolumn ~ ";\n";
252                   }
253                   return result;
254               }());
255         this._key = new_key;
256     }
257 
258     static if (hasForeignKeys!(T))
259     {
260         mixin(createForeignKeyPropertyConverter!(T));
261     }
262 
263     mixin(createConstraintStructs!(T, ClusteredIndexAttribute.name));
264 }
265 
266 ///
267 unittest
268 {
269     class Candy
270     {
271     private:
272         string _name;
273         int _ranking;
274     public:
275         // name is the primary key
276         @PrimaryKeyColumn @NotNull
277         @property string name() const nothrow pure @safe @nogc
278         {
279             return _name;
280         }
281         @property void name(string value)
282         {
283             setter(_name, value);
284         }
285         // ranking must be unique among all the other records
286         @UniqueConstraintColumn!("uc_Candy_ranking")
287         @property int ranking() const nothrow pure @safe @nogc
288         {
289             return _ranking;
290         }
291         // making sure that ranking will always be above 0
292         @CheckConstraint!(a => a > 0, "chk_Candy_ranking")
293         @property void ranking(int value)
294         {
295             setter(_ranking, value);
296         }
297         this(string name, int ranking)
298         {
299             this._name = name;
300             this._ranking = ranking;
301             initializeKeyedItem();
302         }
303 
304         // The primary key is now the clustered index as it is by default
305         mixin KeyedItem!(PrimaryKeyColumn);
306     }
307 
308     // below is what is created when you include the mixin KeyedItem
309     // ClusteredIndex is alias'd as PrimaryKey since we said the
310     // primary key is our clustered index above.
311     // this also creates a uc_Candy_ranking struct and key since
312     // we labeled ranking with @UniqueConstraintColumn!("uc_Candy_ranking")
313     enum candyStructs =
314 `public:
315     final alias PrimaryKey = ClusteredIndex;
316     final alias PrimaryKey_key = key;
317     final struct uc_Candy_ranking
318     {
319         typeof(Candy._ranking) ranking;
320         mixin opAAKey!(uc_Candy_ranking);
321     }
322     final @property uc_Candy_ranking uc_Candy_ranking_key() const nothrow pure @safe @nogc
323     {
324         auto _uc_Candy_ranking_key = uc_Candy_ranking();
325         _uc_Candy_ranking_key.ranking = this._ranking;
326         return _uc_Candy_ranking_key;
327     }
328 `;
329     import db_constraints.utils.meta : createConstraintStructs;
330     static assert(createConstraintStructs!(Candy, "PrimaryKey") == candyStructs);
331     assert(createConstraintStructs!(Candy, "PrimaryKey") == candyStructs);
332 
333 
334     // source: http://www.bloomberg.com/ss/09/10/1021_americas_25_top_selling_candies/10.htm
335     auto i = new Candy("Opal Fruit", 17);
336 
337     // i does not contain changes
338     assert(!i.containsChanges);
339 
340     auto pk = Candy.PrimaryKey("Opal Fruit");
341     // the key property is the clustered index
342     // since we said the primary key is the clustered index
343     // i.key and pk are equal
344     assert(i.key == pk);
345     // PrimaryKey_key is an alias for key
346     assert(i.key == i.PrimaryKey_key);
347     // the primary key struct has member name since that was marked
348     // with @PrimaryKeyColumn
349     assert(i.key.name == pk.name);
350     assert(i.name == pk.name);
351 
352     auto j = new Candy("Opal Fruit", 16);
353     // since name is the primary key i and j are equal because
354     // the names are equal
355     // even though the ranking is different
356     assert(i.key == j.key);
357     assert(i.ranking != j.ranking);
358 
359     // in 1967 Opal Fruits came to America and changed its name
360     i.name = "Starburst";
361     // i now contains changes since we changed the name
362     assert(i.containsChanges);
363     i.markAsSaved();
364     // once we mark it as saved it no longer contains changes
365     assert(!i.containsChanges);
366 
367     // by changing the name it also changes the primary key
368     // so now i.key should not equal the pk we defined above
369     // or j.key
370     assert(i.key != pk);
371     assert(i.key != j.key);
372 
373     import std.exception : assertThrown;
374     import db_constraints.db_exceptions : CheckConstraintException;
375     // we expect setting the ranking to 0 will result in an exception
376     // since we labeled that column with
377     // @CheckConstraint!(a => a > 0, "chk_Candy_ranking")
378     assertThrown!CheckConstraintException(i.ranking = 0);
379 }